#!/bin/bash
# nchainz - start an arbitrary number of chains and relayers

progname=$(basename -- "$0")
thisdir=$(dirname -- ${BASH_SOURCE[0]})

trap 'kill $(jobs -p) 2>/dev/null' EXIT 

BASEDIR=$progname
DEFAULT_CONFIG=gaia
DEFAULT_PORT=transfer
SKIP=no

RELAYER_DIR=$(cd "$thisdir/.." && pwd)
CONFIGS_DIR="$RELAYER_DIR/configs"

CHAIN_RE='^([^=]+)=(.+)$'
LINK_RE='^([^:]+):([^:]+)$'
PORT_RE='^([^@]+)@(.*)$'

CHAINS=()
LINKS=()

# Ensure jq is installed
if [[ ! -x "$(which jq)" ]]; then
  echo "jq (a tool for parsing json in the command line) is required..."
  echo "https://stedolan.github.io/jq/download/"
  exit 1
fi

nextID=0
generate_id() {
  config=$1
  while true; do
    id="ibc$nextID"
    found=no
    for chain in ${CHAINS[@]}; do
      case $chain in
      "$id="*)
        found=yes
        break
      esac
    done
    if [[ $found != yes ]]; then
      return
    fi
    nextID=$(( $nextID + 1 ))
  done
}

IDS=()
CONFIGS=()
JSONS=()
SRCS=()
SRCPORTS=()
DSTS=()
DSTPORTS=()

validate() {
  status=0
  # Have at least one default.
  if [[ ${#CHAINS[@]} -eq 0 ]]; then
    generate_id "$DEFAULT_CONFIG"
    CHAINS+=( "$id=$DEFAULT_CONFIG" )
  fi

  for chain in ${CHAINS[@]} LAST_CHAIN; do
    if [[ $chain == LAST_CHAIN ]]; then
      if [[ ${#CHAINS[@]} -ge 2 ]]; then
        # Don't need more config
        break
      fi
      # We need at least two chains, so use the same config.
      config=${CONFIGS[0]}
      generate_id "$config"
      chain="$id=$config"
      CHAINS+=( "$chain" )
    fi
    if [[ $chain =~ $CHAIN_RE ]]; then
      id=${BASH_REMATCH[1]}
      config=${BASH_REMATCH[2]}
      IDS+=( "$id" )
      json=$CONFIGS_DIR/$config.json
      if [[ ! -f $json ]]; then
        echo 1>&2 "$progname: error: CHAIN \`$chain' config \`$json' does not exist"
        status=1
      fi
      CONFIGS+=( "$config" )
      JSONS+=( "$json" )
    else
      echo 1>&2 "$progname: error: invalid CHAIN specification \`$chain'"
      status=1
    fi
  done

  # Default links as a circuit around the chains.
  if [[ ${#LINKS[@]} -eq 0 ]]; then
    prior=
    for id in ${IDS[@]}; do
      if [[ -n $prior ]]; then
        LINKS+=( "$prior:$id" )
      fi
      prior=$id
    done
    if [[ -z $prior ]]; then
      echo 1>&2 "$progname: error: no configured LINKS"
      status=1
    elif [[ $prior != ${IDS[1]} ]]; then
      LINKS+=( "$prior:${IDS[0]}" )
    fi
  fi

  for link in ${LINKS[@]}; do
    if [[ $link =~ $LINK_RE ]]; then
      src=${BASH_REMATCH[1]}
      dst=${BASH_REMATCH[2]}
      if [[ $src =~ $PORT_RE ]]; then
        srcport=${BASH_REMATCH[1]}
        src=${BASH_REMATCH[2]}
      else
        srcport=$DEFAULT_PORT
      fi
      if [[ $dst =~ $PORT_RE ]]; then
        dstport=${BASH_REMATCH[1]}
        dst=${BASH_REMATCH[2]}
      else
        dstport=$DEFAULT_PORT
      fi
      found_src=no
      found_dst=no
      for id in ${IDS[@]}; do
        [[ $id != $src ]] || found_src=yes
        [[ $id != $dst ]] || found_dst=yes
      done
      if [[ $found_src != yes ]]; then
        echo 1>&2 "$progname: error: LINK \`$link' source ID \`$src' does not match a CHAIN"
        status=1
      fi
      if [[ $found_dst != yes ]]; then
        echo 1>&2 "$progname: error: LINK \`$link' destination ID \`$dst' does not match a CHAIN"
        status=1
      fi
      SRCS+=( "$src" )
      SRCPORTS+=( "$srcport" )
      DSTS+=( "$dst" )
      DSTPORTS+=( "$dstport" )
    else
      echo 1>&2 "$progname: error: invalid LINK specification \`$link'"
      status=1
    fi
  done
  [[ $status -eq 0 ]] || usage $status
}

show_plan() {
  echo "Here's the plan:"
  for i in ${!IDS[@]}; do
    id=${IDS[$i]}
    json=${JSONS[$i]}
    echo " create $id from $json"
  done
  for i in ${!SRCS[@]}; do
    srcport=${SRCPORTS[$i]}
    src=${SRCS[$i]}
    dstport=${DSTPORTS[$i]}
    dst=${DSTS[$i]}
    echo " link $srcport@$src to $dstport@$dst"
  done
}

usage() {
  status=$1
  if [[ $status -eq 0 ]]; then
    cat <<EOF
Usage: $progname COMMAND [OPTIONS]...

COMMAND is one of:
  help [CMD]    display help information for CMD (default: main)
  init ...      create a configuration directory
  chains        start chains running per \`init'
  clients       start clients running per \`init'
  relay         link chains together with relayers per \`init'
  run           both chains and clients
EOF
  else
    echo "Try \`$progname --help' for more information"
  fi
  exit $status
}

do_help() {
  cmd="$1"
  case $cmd in
  help)
    cat <<EOF
Usage: $progname help [COMMAND]

Display usage information for COMMAND.
EOF
    ;;
  init)
    cat <<EOF
Usage: $progname init [skip] [CONFIG|CHAIN|LINK]...

Create a configuration for running IBC chains and relayer links.

Options:
  --basedir=DIR  create DIR [default=\`$progname']
  --help         display this help

If \`skip' is provided, files will be deleted without prompting.

CONFIGs are found in \`$CONFIGS_DIR/CONFIG.json'.
If no arguments are supplied, use \`$DEFAULT_CONFIG'.

Create configuration in \`$DATA' for:
 each CHAIN, which is \`ID=CONFIG'
 each LINK, which is \`SRC:DST',
  where each SRC or DST is \`PORT@ID',
    or just \`ID' to use \`$DEFAULT_PORT@ID'.

If no LINKS are supplied, use a circuit around the CHAINS.
EOF
    ;;
  chains)
    cat <<EOF
Usage: $progname chains [skip]

Start chains running in a directory created by \`$progname init'.

If \`skip' is provided, files will be deleted without prompting.
EOF
    ;;
  clients)
    cat <<EOF
Usage: $progname clients [skip]

Start clients running in a directory created by \`$progname init'.

If \`skip' is provided, files will be deleted without prompting.
EOF
   ;;
  relay)
    cat <<EOF
Usage: $progname relay [skip]

Start relayers in a directory created by \`$progname init'.

If \`skip' is provided, files will be deleted without prompting.
EOF
    ;;
  *)
    usage 0
    ;;
  esac
  exit 0
}

args=${1+"$@"}
while [[ $# -gt 0 ]]; do
  case "$1" in
  --help)
    usage 0
    ;;
  --basedir=*)
    BASEDIR=$(echo "$1" | sed -e 's/^[^=]*//')
    ;;
  --basedir)
    shift
    BASEDIR=$1
    ;;
  -*)
    echo 1>&2 "$progname: unrecognized option \`$1'"
    usage 1
    ;;
  *)
    case $COMMAND in
    "") COMMAND=$1 ;;
    init)
      case "$1" in
      skip) SKIP=yes ;;
      norun) SKIP=norun ;;
      *=*) CHAINS+=( "$1" ) ;;
      *:*) LINKS+=( "$1" ) ;;
      *)
        generate_id "$1"
        CHAINS+=( "$id=$1" )
        ;;
      esac
      ;;
    help)
      do_help "$1"
      ;;
    *)
      break
      ;;
    esac
  esac
  shift
done

mkdir -p "$BASEDIR"
BASEDIR=$(cd "$BASEDIR" && pwd)
NCONFIG="$BASEDIR/nchainz.cfg"
DATA="$BASEDIR/data"
LOGS="$BASEDIR/logs"

case $COMMAND in
"")
  echo 1>&2 "$progname: you must specify a COMMAND"
  usage 1
  ;;
help)
  usage 0
  ;;
init)
  # Just continue.
  ;;
chains)
  source "$NCONFIG"

  # Ensure user understands what will be deleted
  if [[ -d $DATA ]] && [[ $1 != skip ]]; then
    read -p "$progname: will delete $DATA folder. Do you wish to continue? (y/N): " -n 1 -r
    echo 
    if [[ ! $REPLY =~ ^[Yy]$ ]]; then
        exit 1
    fi
  fi
  rm -rf "$DATA"

  echo "mkdir $DATA"
  mkdir -p "$DATA"

  set -e

  echo "Generating chain configurations..."
  cd "$DATA"

  if [[ "$(uname)" == Darwin ]]; then
    sedi() {
      sed -i '' ${1+"$@"}
    }
  else
    sedi() {
      sed -i ${1+"$@"}
    }
  fi

  base=0
  for i in ${!IDS[@]}; do
    ID=${IDS[$i]}
    json=${JSONS[$i]}
    DAEMON=$(jq -r .daemon $json)
    CLI=$(jq -r .cli $json)

    DAEMON_TESTNET=$(jq -r '."daemon-testnet"' "$json")
    [[ $DAEMON_TESTNET != null ]] || DAEMON_TESTNET='echo | $DAEMON testnet -o $chainid --v 1 --node-daemon-home=$DAEMON --chain-id $chainid --node-dir-prefix n --keyring-backend test'

    chainid="$ID"
    p1317=$(( 1317 + $i ))
    p26656=$(( 26656 - $i * 100 ))
    p26657=$(( 26657 - $i * 100 ))
    p26658=$(( 26658 - $i * 100 ))
    p26660=$(( 26660 - $i * 100 ))
    p6060=$(( 6060 + $i ))
    p9090=$(( 9090 + $i ))

    eval $DAEMON_TESTNET || true

    cfgpth="$DATA/$chainid/n0/$DAEMON/config/config.toml"
    # TODO: Just index *some* specified tags, not all
    sedi 's/index_all_keys = false/index_all_keys = true/g' "$cfgpth"

    # Set proper defaults and change ports
    sedi 's/"leveldb"/"goleveldb"/g' "$cfgpth"
    sedi "s#:26656#:$p26656#g" "$cfgpth"
    sedi "s#:26657#:$p26657#g" "$cfgpth"
    sedi "s#:26658#:$p26658#g" "$cfgpth"
    sedi "s#:26660#:$p26660#g" "$cfgpth"
    sedi "s#:6060#:$p6060#g" "$cfgpth"

    # Make blocks run faster than normal
    sedi 's/timeout_commit = "[0-9]*s"/timeout_commit = "1s"/g' "$cfgpth"
    sedi 's/timeout_propose = "[0-9]*s"/timeout_propose = "1s"/g' "$cfgpth"

    apppth="$DATA/$chainid/n0/$DAEMON/config/app.toml"
    sedi "s#:1317#:$p1317#g" "$apppth"
    sedi "s#:9090#:$p9090#g" "$apppth"
  done

  for i in ${!IDS[@]}; do
    json=${JSONS[$i]}
    CLI=$(jq -r .cli $json)
    DAEMON=$(jq -r .daemon $json)
    CONFIG=${CONFIGS[$i]}
    ID=${IDS[$i]}
    chainid=$ID

    # Allow the config to override the start command.
    DAEMON_HOME="$DATA/$chainid/n0/$DAEMON"
    DAEMON_START=$(jq -r '."daemon-start"' "$json")
    [[ $DAEMON_START != null ]] || DAEMON_START='$DAEMON --home "$DAEMON_HOME" start --pruning=nothing'

    echo "'$DAEMON start' ($chainid) logs in $LOGS/$ID.log"
    (
      eval $DAEMON_START
    ) >> "$LOGS/$ID.log" 2>&1 &
  done

  echo "Ctrl-C exits, or you can background this script..."
  wait
  exit 0
  ;;
clients)
  source "$NCONFIG"
  RELAYER_CONF="$HOME/.relayer"

  set -e

  # Ensure gopath is set and go is installed
  GOBIN=${GOBIN-${GOPATH-$HOME/go}/bin}
  if [[ ! -d $GOPATH ]] || [[ ! -d $GOBIN ]] || [[ ! -x "$(which go)" ]]; then
    echo "Your \$GOPATH is not set or go is not installed,"
    echo "ensure you have a working installation of go before trying again..."
    echo "https://golang.org/doc/install"
    exit 1
  fi

  # Ensure user understands what will be deleted
  if [[ -d $RELAYER_CONF ]] && [[ "$1" != "skip" ]]; then
    read -p "$progname: will delete $RELAYER_CONF folder. Do you wish to continue? (y/n): " -n 1 -r
    echo
    if [[ ! $REPLY =~ ^[Yy]$ ]]; then
        exit 1
    fi
  fi

  (
    cd "$RELAYER_DIR"
    rm -rf "$RELAYER_CONF"

    echo "Building Relayer..."
    make install
  )

  echo "Generating rly configurations..."
  rly config init
  rly config add-chains "$BASEDIR/config/chains"

  sleep 2

  for i in ${!IDS[@]}; do
    ID=${IDS[$i]}
    json=${JSONS[$i]}
    CLI=$(jq -r .cli $json)
    DAEMON=$(jq -r .daemon $json)
    chainid=$ID
    (
      try=0
      f="$DATA/$chainid/n0/$DAEMON/key_seed.json"
      while [[ ! -f $f ]]; do
        try=$(( $try + 1 ))
        echo "no $f"
        echo "$chainid $CLI is not yet ready (try=$try)"
        sleep 1
      done
      SEED=$(jq -r '.secret' "$f")
      echo "Key $(rly keys restore $chainid testkey "$SEED") imported from $chainid to relayer..."

      try=0
      f="$DATA/$chainid/n0/$DAEMON/config/genesis.json"
      while [[ ! -f $f ]]; do
        try=$(( $try + 1 ))
        echo "no $f"
        echo "$chainid is not yet ready (try=$try)"
        sleep 1
      done
      echo "Creating light client for $chainid ($DAEMON)..."
      try=0
      while ! rly light init $chainid -f >> "$LOGS/light-$chainid.log" 2>&1; do
        try=$(( $try + 1 ))
        echo "$chainid light client not yet ready (try=$try)"
        sleep 1
      done
      try=$(( $try + 1 ))
      echo "$chainid light client initialized (try=$try)"
    ) &
  done

  # Let all our chains initialise.
  wait
  rly config add-paths "$BASEDIR/config/paths"

  for i in ${!IDS[@]}; do
    ID=${IDS[$i]}
    json=${JSONS[$i]}
    
    CMD=$(jq -r '."post-light-client"' $json)

    [[ $CMD != null ]] || continue
    (
      chainid=$ID
      eval $CMD
    ) &
  done
  echo "Waiting for clients (Control-C to exit)..."
  wait
  exit 0
  ;;

connect)
  . "$NCONFIG"

  paths=
  for i in ${!SRCS[@]}; do
    src=${SRCS[$i]}
    dst=${DSTS[$i]}

    path="path-$i"
    paths="$paths $path"
    echo "Starting 'rly tx conn $path' ($src<>$dst) logs in $LOGS/$path.log"
    (
      try=0
      while ! rly tx conn $path --timeout=3s -d >> "$LOGS/$path.log" 2>&1; do
        try=$(( $try + 1 ))
        echo "$path tx conn not yet ready (try=$try)"
        sleep 1
      done
      try=$(( $try + 1 ))
      echo "$path tx conn initialized (try=$try)"
    ) &
  done

  wait
  echo "==============================="
  echo "=== All connections initialized"
  for path in $paths; do
    tail -1 "$LOGS/$path.log"
  done
  echo "==============================="
  exit 0
  ;;

relay)
  . "$NCONFIG"

  paths=
  for i in ${!SRCS[@]}; do
    src=${SRCS[$i]}
    dst=${DSTS[$i]}

    path="path-$i"
    paths="$paths $path"
    echo "Starting 'rly tx link $path' ($src<>$dst) logs in $LOGS/$path.log"
    (
      try=0
      while ! rly tx link $path --timeout=3s -d >> "$LOGS/$path.log" 2>&1; do
        try=$(( $try + 1 ))
        echo "$path tx link not yet ready (try=$try)"
        sleep 1
      done
      try=$(( $try + 1 ))
      echo "$path tx link initialized (try=$try)"

      # Run the relayer for future packets.
      rly start -d "$path"
    ) &
  done

  echo "Check the state of the links using 'rly paths list' to see when they are ready..."
  echo "(Ctrl-C exits, or you can background this job)"
  todo=$paths
  while :; do
    rly paths list
    remaining=$todo
    todo=
    for path in $remaining; do
      if [[ $(rly paths show "$path" --json | jq -r .status.channel) != true ]]; then
        todo="$todo $path"
      fi
    done
    if [[ -z $todo ]]; then
      break
    fi
    sleep 5
  done

  echo "All paths initialized, waiting for relayers (Control-C to exit)..."
  wait
  exit 0
  ;;
run)
  mv nchainz/logs nchainz/logs-$(date +%Y%m%dT%H:%M:%S) || true
  mkdir -p nchainz/logs
  "$0" chains skip &
  "$0" clients skip &
  wait
  exit 0
  ;;
*)
  echo 1>&2 "$progname: unrecognized COMMAND \`$COMMAND'"
  usage 1
  ;;
esac

# This is the "init" command:
validate
show_plan

# Ensure user understands what will be deleted
echo
if [[ -e $BASEDIR ]] && [[ $SKIP != yes ]]; then
  read -p "$progname: will delete $BASEDIR folder. Do you wish to continue? (y/n): " -n 1 -r
  echo
  if [[ ! $REPLY =~ ^[Yy]$ ]]; then
      exit 1
  fi
fi

rm -rf "$BASEDIR"
mkdir -p "$BASEDIR/config/paths" "$BASEDIR/config/chains" "$LOGS"

echo "creating $NCONFIG"
cat <<EOF >"$NCONFIG"
# $NCONFIG - $progname configuration script
# DO NOT EDIT - Automatically generated by $progname $args
DATA='$BASEDIR/data'
LOGS='$BASEDIR/logs'
IDS=( $(printf "'%s' " "${IDS[@]}"))
CONFIGS=( $(printf "'%s' " "${CONFIGS[@]}"))
JSONS=( $(printf "'%s' " "${JSONS[@]}"))
SRCS=( $(printf "'%s' " "${SRCS[@]}"))
SRCPORTS=( $(printf "'%s' " "${SRCPORTS[@]}"))
DSTS=( $(printf "'%s' " "${DSTS[@]}"))
DSTPORTS=( $(printf "'%s' " "${DSTPORTS[@]}"))
EOF

for i in ${!IDS[@]}; do
  id=${IDS[$i]}
  json=${JSONS[$i]}
  p26657=$(( 26657 - $i * 100 ))
  out="$BASEDIR/config/chains/$id.json"
  echo "creating $out"
  jq ".link + { \"chain-id\": \"$id\", \"rpc-addr\": \"http://localhost:$p26657\", \"key\": \"testkey\" }" "$json" \
    > "$out"
done

for i in ${!SRCS[@]}; do
  src=${SRCS[$i]}
  srcport=${SRCPORTS[$i]}

  dst=${DSTS[$i]}
  dstport=${DSTPORTS[$i]}

  path="path-$i"
  out="$BASEDIR/config/paths/$path.json"
  echo "creating $out"
  cat >"$out" <<EOF
{
  "src": {
    "chain-id": "$src",
    "port-id": "$srcport",
    "order": "unordered"
  },
  "dst": {
    "chain-id": "$dst",
    "port-id": "$dstport",
    "order": "unordered"
  },
  "strategy": {
    "type": "naive"
  }
}
EOF
done

#####################
cat <<EOF

=====================
Done generating $BASEDIR

You can use: \`$progname [run|connect|relay]' at any time.

EOF

if [[ $SKIP == norun ]]; then
  exit 0
fi

if [[ $SKIP != yes ]]; then
  read -p "Do you wish to \`run', and \`connect' right now (y/N)? " -n 1 -r
  echo

  if [[ ! $REPLY =~ ^[Yy]$ ]]; then
    exit 0
  fi
fi

"$0" run skip &
"$0" connect skip &
echo "Hit Control-C to exit"
wait
